# coding=utf-8
'''
Created on Jun 16, 2009

@license: License: LGPL
@copyright: (c) 2009, dogsing.cn
@author: ant-man
'''

from cn.pigersing.moc.lang.mocCallable import mocCallable, MocNativeCall, MocMocNativeCall
from cn.pigersing.moc.lang.mocCollection import mocList, mocDict
from cn.pigersing.moc.lang.mocConstant import mocNone, mocFalse, mocTrue, mocString, mocInt
from cn.pigersing.moc.lang.mocInstance import inst_list, MocTempInst, MocInstance
from cn.pigersing.moc.lang.mocObject import MocRuntimeException
from mocAst import NodeVisitor
from mocContext import MocContext
from mocTranslate import MocTranslator
import cn.pigersing.moc.lang.mocCst as mocCst

#===============================================================================
# MocRuntimeError
#===============================================================================

class MocUndefinedVaribale(MocRuntimeException):pass

class MocIllegalJumpingAddress(MocRuntimeException):pass

class MocCodeSection(object):
    """
    """

    def __init__(self, code_list):
        self.code_list = code_list
        self.__pc = 0
        self.__code_len = len(code_list)
        self.breakpoints = []

    def gen(self):
        """
        Generate next instruction
        """
        return None if (self.pc >= self.__code_len or not self.code_list) else self.code_list[self.pc]

    def pc(): #@NoSelf
        doc = """Program counter""" #@UnusedVariable

        def fget(self):
            return self.__pc

        def fset(self, value):
            self.__pc = value

        return locals()

    pc = property(**pc())


class MocInterpreter(NodeVisitor):
    '''
    classdocs
    '''

    def __init__(self):
        '''
        Constructor
        '''
        self.param_stack = []
        self.ret = MocTempInst(0, mocNone)
        self.__code_stack = []
        self.__env_stack = []
        self.inst_list = inst_list
        # current context at the top __env_stack
        self.ctx = None
        # make a global context
        self.push_newenv()
        # executable code list
        self.code = None
        # register the default native calls
#        self.register_native_calls(register_mocsys_funcs())

    def push_newenv(self, ctx=None):
        """
        push a new context to current environment
        """
        self.ctx = MocContext(self.ctx, self.inst_list, ctx)
        self.__env_stack.append(self.ctx)

    def pop_env(self):
        # dereference the instance
        ret = self.__env_stack.pop()
#        for idx in ret.ctx.values():
#            self.inst_list[idx].clean()
        try :
            self.ctx = self.__env_stack[-1]
        except IndexError :
            return None
        return ret

    def push_newcode(self, code_list=None):
        """
        """
        self.code = MocCodeSection(code_list)
        self.__code_stack.append(self.code)

    def pop_code(self):
        ret = self.__code_stack.pop()
        try :
            self.code = self.__code_stack[-1]
        except IndexError :
            return None
        return ret

    def gen(self):
        ret = self.code.gen()
        self.code.pc += 1
        return ret

    def register_native_calls(self, native_list):
        """
        TODO:
        """
        add_ctx = {}
        for native in native_list :
            assert isinstance(native, MocNativeCall)
            add_ctx[native.name] = mocCallable.new_inst(native).id
        self.ctx.ctx.update(add_ctx)

    def interp(self, code=None):
        if not code:
            return
        self.push_newcode(code)

        cmd = self.gen()
        while cmd:
            cmd.accept(self)
            cmd = self.gen()
        #
        self.pop_code()

#===============================================================================
#  
# The following is to interpret each instruction
# 
#===============================================================================

    def __get_inst(self, name):
        # get source instance
        if isinstance(name, MocInstance) :
            return name
        else :  # source is a symbol or a undefined variable
            ret = self.ctx.get(name)
            if not ret:
                # raise an exception, the may happen when variable is referenced
                # at right hand side
                raise MocUndefinedVaribale("Instance of '%s' is undefined" % name)
            return ret

#------------------------------------------------------------------------------ 
## Module begin and end, declaration

    def visit_Begin(self, node):
        '''visit Begin'''
        self.push_newenv()

    def visit_End(self, node):
        '''visit End'''
        self.pop_env()

    def visit_Enterenv(self, node):
        '''visit Leaveenv'''
        self.push_newenv()

    def visit_Leaveenv(self, node):
        '''visit Leaveenv'''
        self.pop_env()

#------------------------------------------------------------------------------ 
## Assignment, Binary and Unary operation 

    def visit_AssignOp(self, node):
        '''visit AssignOp'''
        # get source instance
        src = self.__get_inst(node.src)
        # get target instance
        try :
            target = self.__get_inst(node.target)
            # assign the source to target
            target.assign(src)
        except MocUndefinedVaribale :
            # this must be a new identifier, so make a reference to the source
            if src.type_equals(mocNone) :
                src = mocNone.new_inst()
            self.ctx.set(node.target, src)
            target = self.__get_inst(node.target)
        return target

    def visit_BinaryOp(self, node):
        '''visit BinaryOp'''
        left = self.__get_inst(node.left)
        right = self.__get_inst(node.right)
        # perform the operation specified by op
        result = left.binary_op(node.op, right)
        if node.ret :
            ret = self.__get_inst(node.ret)
            ret.assign(result)
            return ret
        return result

    def visit_UnaryOp(self, node):
        '''visit UnaryOp'''
        right = self.__get_inst(node.right)
        ret = self.__get_inst(node.ret)
        # perform the operation specified by op
        ret.assign(right.unary_op(node.op))
        return ret

#------------------------------------------------------------------------------ 
## Jump instruction, includes IfTrue and IfFalse and Goto

    def visit_IfTrue(self, node):
        '''visit IfTrue'''
        ret = node.test.accept(self)
        if ret == mocTrue :
            node.goto.accept(self)

    def visit_IfFalse(self, node):
        '''visit IfFalse'''
        ret = node.test.accept(self)
        if ret == mocFalse :
            node.goto.accept(self)

    def visit_Goto(self, node):
        '''visit Goto'''
        if node.label.lineno == 0:
            raise MocIllegalJumpingAddress("Illegal jumping address")
        self.code.pc = node.label.lineno

#------------------------------------------------------------------------------ 
## Function reference
    def visit_CallableDef(self, node):
        param_code = node.params
        code_list = node.body
        func = mocCallable.new_inst((param_code, code_list))
        # save current and parent context, so the new callable can reference
        # the instances in those scope
        ctx = {}
        ctx.update(self.ctx.parent.ctx if self.ctx.parent else {})
        ctx.update(self.ctx.ctx)
        func.value.update_sealed_ctx(ctx)
        self.interp([mocCst.AssignOp(func, node.ret)])

    def visit_Param(self, node):
        '''visit Param'''
        # push the parameter instance in param_stack
        self.param_stack.append(self.__get_inst(node.val))

    def visit_Getret(self, node):
        '''visit Getret'''
        #
        ret = self.__get_inst(node.ret)
        ret.assign(self.__get_inst(self.ret))

    def visit_Setret(self, node):
        '''visit Setret'''
        #
        self.ret.assign(self.__get_inst(node.src))

    def visit_Call(self, node):
        '''visit Call'''
        # function instance
        func_inst = self.__get_inst(node.name)
        if not func_inst.type_equals(mocCallable) :
            raise MocRuntimeException("%s is not a callable object!" % node.name)
        parent = self.__get_inst(func_inst.parent)
        # get real function value
        func = func_inst.value
        # invoke native function
        if isinstance(func, MocNativeCall) :
            # get the real parameters
            r_param_num = node.param_num
            r_params = []
            for i in range(r_param_num) :
                r_params.append(self.__get_inst(self.param_stack.pop()))
            if isinstance(func, MocMocNativeCall) :
                # this is special for build-ins or some native functions that operating
                # MocInstance in a hierarchy access
                if not parent is func_inst :
                    r_params = [parent] + r_params
            ret = func.invoke(r_params)
            # generate a fake instruction, set the return value
            self.interp([mocCst.Setret(ret)])
        else :
            # get parameter count
            f_param_num = func.param_count
            r_param_num = node.param_num
            # get parameters
            f_params = func.params
            # check the parameters
            d = f_param_num - r_param_num
            if d < 0 :
                raise MocRuntimeException("'%s' need %d but %d are given" % (node.name, f_param_num, r_param_num))
            elif d > 0 :
                if not f_params[-d][1] :
                    raise MocRuntimeException("'%s' lack of parameters, need %d but %d are given"
                                              % (node.name, f_param_num, r_param_num))
            # get parameters from param_stack
            r_params = []
            for i in range(r_param_num) :
                r_params.append(self.__get_inst(self.param_stack.pop()))
            # if this callable is referred as a field of user-made object, then "this" is represent its
            # parent, say, if foo.bar(), the "this" in bar is the reference of foo,
            # so you can even change the value of foo through this just like other reference
            ctx = {"this":parent.id}
            # set non-default parameters
            for i in range(r_param_num) :
                ctx[f_params[i][0]] = r_params[i].id
            # set default parameters
            param_code_list = []
            for i in range(r_param_num, f_param_num):
                # merge the parameter codes
                param_code_list.extend(f_params[i][1])
            # merge the sealed context, this may be a closure invoke
            ctx.update(func.sealed_ctx)
            # create a new context for function
            self.push_newenv(ctx)
            # execute the parameter code
            self.interp(param_code_list)
            # push new code into execution stack, and execute the code
            self.interp(func.code_list)
            # pop up the outer context
            ctx = self.pop_env()


#------------------------------------------------------------------------------ 
## Object reference

    def visit_Objassign(self, node):
        '''visit Objassign'''
        try:
            obj = self.__get_inst(node.obj)
        except MocUndefinedVaribale :
            # user may want to make a object, so we generate a dictionary instance
            # to simulate a objects
            obj = mocDict.new_inst()
            self.ctx.set(node.obj, obj)
        field = obj.get_field(node.field)
        if not field:
            field = mocNone.new_inst()
            obj.add_field(node.field, field)
        field.parent = obj
        self.interp([mocCst.AssignOp(node.src, field)])

    def visit_Objref(self, node):
        '''visit Objref'''
        obj = self.__get_inst(node.obj)
        field = obj.get_field(node.field)
        if not field:
            raise MocRuntimeException("Object doesn't have field named '%s'" % node.field)
        field.parent = obj
        self.interp([mocCst.AssignOp(field, node.ret)])

#------------------------------------------------------------------------------ 
## Collection reference

    def visit_Listitem(self, node):
        '''visit Listitem'''
        # the same as Param val
        # push the parameter instance in param_stack
        # make a reference, in case of val is a MonTempInstance
        self.param_stack.append(self.__get_inst(node.val).refer())

    def visit_Newlist(self, node):
        '''visit Newlist'''
        count = node.num
        # get list items
        items = []
        for i in range(count) :
            items.append(self.__get_inst(self.param_stack.pop()))
        # reverse them
        items = reversed(items)
        ls = mocList.new_inst(items)
        self.interp([mocCst.AssignOp(ls, node.ret)])

    def visit_Range(self, node):
        '''visit Range'''
        # push the parameter instance in param_stack
        low = self.__get_inst(node.low)
        high = self.__get_inst(node.high)
        self.param_stack.append((low, high))

    def visit_Slice(self, node):
        '''visit Slice'''
        low, high = self.param_stack.pop()
        src = self.__get_inst(node.src)
        src = src.slice(low, high)
        self.interp([mocCst.AssignOp(src, node.ret)])

    def visit_Dictitem(self, node):
        '''visit Dictitem'''
        # push the parameter instance in param_stack
        key = self.__get_inst(node.key)
        # make a reference, in case of val is a MonTempInstance
        val = self.__get_inst(node.val).refer()
        if key.type_equals(mocString) or key.type_equals(mocInt) :
            return  self.param_stack.append((key, val))
        raise MocRuntimeException("Key type error, key must be either string or int ")

    def visit_Newdict(self, node):
        '''visit Newdict'''
        count = node.num
        # get list items
        items = {}
        # generate fields from parameter stack
        fields = []
        for i in range(count) :
            key, val = self.param_stack.pop()
            items[key.pyval()] = val
            if key.type_equals(mocString):
                fields.append((key.pyval(), val))
        dic = mocDict.new_inst(items)
        # add new field to dictionary object
        for field in fields :
            dic.add_field(*field)
        self.interp([mocCst.AssignOp(dic, node.ret)])

    def visit_Adddictkey(self, node):
        '''visit Adddictkey'''
        ret = mocNone.new_inst()
        ret.assign(self.ret)
        target = self.__get_inst(node.target)
        key = self.__get_inst(node.key)
        target.add_item(key, ret)

    def visit_Collref(self, node):
        '''visit Collref'''
        coll = self.__get_inst(node.coll)
        subidx = self.__get_inst(node.subscript)
        # get the reference specified by subscript
        ret = self.__get_inst(coll.get_item(subidx))
        # perform the setRet instruction
        self.interp([mocCst.Setret(ret)])


if __name__ == '__main__':

    from mocParser import MocParser, ParseError
    from mocTranslate import MocTranslator
    from cn.pigersing.moc.lang.mocSys import init_intp
    import sys

    src = r'''
     foo = {
         "bar" : def (x) {
             this.x = x ;
         },
         "x":0
        };
        
    print(sys.version);
    print(sys.author);
    print(foo.x);
    '''
    parser = MocParser(lex_optimize=True, yacc_debug=True, yacc_optimize=True)
    try:
        t = parser.parse(src, 'my.moc', debuglevel=0)
        intp = MocInterpreter()
        init_intp(intp, sys.stdout)
        s = MocTranslator(sys.stdout)
        if t :
            code = s.translate(t)
            intp.interp(code)
    except MocRuntimeException, e :
        print >> sys.stderr, 'runtime error ', e
    except ParseError, e :
        print >> sys.stderr, 'parse error ', e
    except Exception, e:
        print >> sys.stderr, 'error ', e
        import traceback;traceback.print_exc()

